package org.luckyjourney.schedul;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.luckyjourney.constant.AuditStatus;
import org.luckyjourney.constant.RedisConstant;
import org.luckyjourney.entity.Setting;
import org.luckyjourney.entity.video.Video;
import org.luckyjourney.entity.vo.HotVideo;
import org.luckyjourney.service.SettingService;
import org.luckyjourney.service.video.VideoService;
import org.luckyjourney.util.RedisCacheUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @description: 热度排行榜
 * @Author: Xhy
 * @CreateTime: 2023-10-31 00:50
 */
@Component
public class HotRank {

    @Autowired
    private VideoService videoService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SettingService settingService;

    @Autowired
    private RedisCacheUtil redisCacheUtil;

    //定义序列化器和对象映射器 使用 Jackson2JsonRedisSerializer 进行对象的 JSON 序列化，将对象序列化为 JSON 字符串
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    //创建 ObjectMapper 对象来实现对象与 JSON 之间的转换
    ObjectMapper om = new ObjectMapper();

    {
        jackson2JsonRedisSerializer.setObjectMapper(om);
    }

    /**
     * 热度排行榜
     */
    @Scheduled(cron = "0 0 */1 * * ?") //每隔一小时执行一次
    public void hotRank() {
        // 利用小根堆控制数量  大小为 10 的优先队列（PriorityQueue）存储前 10 个热度最高的视频   指定了优先队列的排序方式，即按照视频的热度进行排序
        /**
         * PriorityQueue 默认实现的是小根堆（Min-Heap），
         * 也就是说队列头部是最小元素。这意味着当你访问或移除队列的元素时（例如使用peek，poll等方法），你会得到队列中的最小元素。
         */
        final TopK topK = new TopK(10, new PriorityQueue<HotVideo>(10, Comparator.comparing(HotVideo::getHot)));
        long limit = 1000;
        //利用id为索引 进行快速分页
        long id = 0;
        //查询视频列表 查询条件包括审核通过且未开放的视频，并按照视频 ID 大于指定的id值进行分页查询 每次拿1000个
        List<Video> videos = videoService.list(new LambdaQueryWrapper<Video>()
                .select(Video::getId, Video::getShareCount, Video::getHistoryCount, Video::getStartCount, Video::getFavoritesCount,
                        Video::getGmtCreated, Video::getTitle)
                .gt(Video::getId, id)
                .eq(Video::getAuditStatus, AuditStatus.SUCCESS).eq(Video::getOpen, 0)
                .last("limit " + limit));

        //对查询到的视频列表进行遍历，计算每个视频的热度值，并将其封装成 HotVideo 对象，然后添加到 TopK 对象中
        while (!ObjectUtils.isEmpty(videos)) {
            for (Video video : videos) {
                Long shareCount = video.getShareCount();                //分享数 * 1.0
                Double historyCount = video.getHistoryCount() * 0.8;    //浏览次数 * 0.8
                Long startCount = video.getStartCount();                //点赞数 * 1.0
                Double favoritesCount = video.getFavoritesCount() * 1.5;//收藏数 * 1.5
                final Date date = new Date();
                long t = date.getTime() - video.getGmtCreated().getTime(); //时间差
                // 随机获取6位数,用于去重
                final double v = weightRandom();
                // 热度计算 半衰期公式
                final double hot = hot(shareCount + historyCount + startCount + favoritesCount + v, TimeUnit.MILLISECONDS.toDays(t));
                final HotVideo hotVideo = new HotVideo(hot, video.getId(), video.getTitle()); //用HotVideo封装

                topK.add(hotVideo);
            }
            //根据当前列表中最后一个视频的 ID，向后继续查询视频列表，以实现分页加载视频数据的功能
            id = videos.get(videos.size() - 1).getId();
            videos = videoService.list(new LambdaQueryWrapper<Video>()
                    .gt(Video::getId, id) //查询视频的 ID 大于当前列表中最后一个视频的 ID
                    .last("limit " + limit));
        }
        final byte[] key = RedisConstant.HOT_RANK.getBytes(); //hot:rank
        final List<HotVideo> hotVideos = topK.get();          //获取热度排行榜中的视频信息列表
        final Double minHot = hotVideos.get(0).getHot();      //获取热度排行榜中最低热度值，以便后续从 Redis 中移除热度低于此值的视频
        redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (HotVideo hotVideo : hotVideos) {
                final Double hot = hotVideo.getHot();
                try {
                    hotVideo.setHot(null); //hot设为null，因为sortSet会去重，同样的视频可能在前一个热度排行榜中存在，第二次刷榜后，又有这个视频，但是热度不一样，给前端的时候再设置hot
                    // 不这样写铁报错！序列化问题
                    //key sortSet的键名。hot 是视频的热度值，作为sortSet的分数 将视频信息对象序列化为 JSON 字符串，并使用 Jackson 序列化器进行处理
                    connection.zAdd(key, hot, jackson2JsonRedisSerializer.serialize(om.writeValueAsString(hotVideo)));
                } catch (JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
            return null;
        });
        //清理掉热度排行榜中热度值较低的视频，保留热度高于 minHot 的视频
        redisTemplate.opsForZSet().removeRangeByScore(RedisConstant.HOT_RANK, minHot,0);
    }

    // 热门视频,没有热度排行榜实时且重要
    @Scheduled(cron = "0 0 */3 * * ?")
    public void hotVideo() {
        // 分片查询3天内的视频
        int limit = 1000;
        long id = 1;
        List<Video> videos = videoService.selectNDaysAgeVideo(id, 3, limit);
        final Double hotLimit = settingService.list(new LambdaQueryWrapper<Setting>()).get(0).getHotLimit();
        Calendar calendar = Calendar.getInstance();
        int today = calendar.get(Calendar.DATE);

        while (!ObjectUtils.isEmpty(videos)) {
            final ArrayList<Long> hotVideos = new ArrayList<>();// 热门视频id集合
            for (Video video : videos) {
                Long shareCount = video.getShareCount();
                Double historyCount = video.getHistoryCount() * 0.8;
                Long startCount = video.getStartCount();
                Double favoritesCount = video.getFavoritesCount() * 1.5;
                final Date date = new Date();
                long t = date.getTime() - video.getGmtCreated().getTime();
                // 热度计算 半衰期公式
                final double hot = hot(shareCount + historyCount + startCount + favoritesCount, TimeUnit.MILLISECONDS.toDays(t));

                // 大于X热度说明是热门视频
                if (hot > hotLimit) {
                    hotVideos.add(video.getId());
                }

            }
            id = videos.get(videos.size() - 1).getId();
            videos = videoService.selectNDaysAgeVideo(id, 3, limit);
            // RedisConstant.HOT_VIDEO + 今日日期 作为key  达到元素过期效果
            if (!ObjectUtils.isEmpty(hotVideos)){
                String key = RedisConstant.HOT_VIDEO + today;
                redisTemplate.opsForSet().add(key, hotVideos.toArray(new Object[hotVideos.size()]));
                redisTemplate.expire(key, 3, TimeUnit.DAYS);
            }

        }


    }

    static double a = 0.011;

    // 热度计算 半衰期公式
    public static double hot(double weight, double t) {
        return weight * Math.exp(-a * t);
    }

    // 随机获取6位数,用于去重
    public double weightRandom() {
        int i = (int) ((Math.random() * 9 + 1) * 100000);
        return i / 1000000.0;
    }

}
